{
"cells": [
{
"cell_type": "markdown",
"id": "0",
"metadata": {},
"source": [
"# TLE Fitting\n",
"\n",
"Two-Line Element sets (TLEs) are the standard format for distributing satellite orbital elements, but they degrade in accuracy over time. When higher-fidelity state vectors are available (e.g., from GPS or precision orbit determination), it is useful to fit a new TLE to those states.\n",
"\n",
"`satkit.TLE.fit_from_states` performs this fit using Levenberg-Marquardt non-linear least-squares optimization. It tunes the TLE orbital parameters to minimize the difference between the input state positions and the SGP4-predicted positions. Input states are assumed to be in the GCRF frame and are internally rotated to the TEME frame used by SGP4."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1",
"metadata": {},
"outputs": [],
"source": [
"# Imports\n",
"import satkit as sk\n",
"import numpy as np\n",
"import math as m\n",
"\n",
"# Create a high-precision state\n",
"# Altitude for circular orbit\n",
"altitude = 450e3\n",
"\n",
"# Radius & velocity\n",
"r0 = altitude + sk.consts.earth_radius\n",
"v0 = m.sqrt(sk.consts.mu_earth / r0)\n",
"\n",
"# Inclination\n",
"inclination = 15 * m.pi / 180.0\n",
"\n",
"# Create the state (3D position in meters, 3D velocity in meters / second)\n",
"state0 = np.array([r0, 0, 0, 0, v0 * m.cos(inclination), v0 * m.sin(inclination)])\n",
"# Make up an epoch\n",
"time0 = sk.time(2024, 3, 15, 13, 0, 0)\n",
"\n",
"# Propagate the state forward by a day with high-precision propagator\n",
"res = sk.propagate(state0, time0, time0 + sk.duration(days=1.0))\n",
"\n",
"# Get interpolated states every 10 minutes\n",
"times = [time0 + sk.duration(minutes=i) for i in range(0, 1440, 10)]\n",
"states = [res.interp(t) for t in times]\n",
"\n",
"# Fit the TLE\n",
"(tle, fitresults) = sk.TLE.fit_from_states(states, times, time0 + sk.duration(days=0.5)) # type: ignore\n",
"\n",
"# Print the result\n",
"print(tle)\n",
"print(fitresults[\"success\"])"
]
},
{
"cell_type": "markdown",
"id": "2",
"metadata": {},
"source": [
"## Generate Test Data\n",
"\n",
"To demonstrate the fitting, we create a synthetic truth trajectory by propagating a circular orbit at 450 km altitude with satkit's high-precision propagator. We then sample position and velocity states every 10 minutes over one day, and fit a TLE to those samples."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3",
"metadata": {},
"outputs": [],
"source": [
"# Compute position errors (differences between TLE & state)\n",
"\n",
"# Get the positions from sgp4\n",
"(pteme, vteme) = sk.sgp4(tle, times)\n",
"# Rotate positions from TEME to GCRF frame\n",
"pgcrf = [sk.frametransform.qteme2gcrf(t) * p for t, p in zip(times, pteme)]\n",
"# Take difference between state vector and SGP4 positions, and compute norm\n",
"pdiff = [p - s[0:3] for p, s in zip(pgcrf, states)]\n",
"pdiff = np.array([np.linalg.norm(p) for p in pdiff])\n",
"\n",
"\n",
"# Plot position errors\n",
"import matplotlib.pyplot as plt\n",
"import scienceplots # noqa: F401\n",
"plt.style.use([\"science\", \"no-latex\", \"../satkit.mplstyle\"])\n",
"%config InlineBackend.figure_formats = ['svg']\n",
"\n",
"fig, ax = plt.subplots(figsize=(10, 5))\n",
"ax.plot([t.as_datetime() for t in times], pdiff, color=\"black\", linewidth=2)\n",
"ax.set_xlabel(\"Time\")\n",
"ax.set_ylabel(\"Position Error (m)\")\n",
"ax.set_title(\"TLE Fitting Position Errors\")\n",
"fig.autofmt_xdate()\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "4",
"metadata": {},
"source": [
"## Evaluate Fit Quality\n",
"\n",
"Compare the fitted TLE against the original states by propagating the TLE with SGP4, rotating from TEME to GCRF, and computing position differences. Since TLEs are a simplified analytical model, some residual error is expected even with a perfect fit."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}